using System;
using System.Collections.Specialized;
using System.Web;
using System.Text;

namespace ExtremeSwank.Authentication.OpenID
{
    /// <summary>
    /// Provides an OpenID Relying Party (Consumer) compatible with OpenID 1.1 and 2.0 specifications.
    /// </summary>
    [Serializable]
    public class OpenIDConsumer
    {
        #region Fields and Properties

        StateContainer rp;

        /// <summary>
        /// Set to true to enforce use of Directed Identity.
        /// </summary>
        /// <remarks>
        /// If set to true, OpenIDServer must be explicitly set.
        /// </remarks>
        public bool UseDirectedIdentity
        {
            get { return rp.UseDirectedIdentity; }
            set { rp.UseDirectedIdentity = value; }
        }

        /// <summary>
        /// Gets or sets the URL that will serve as the base root of trust - defaults to current domain
        /// </summary>
        public string TrustRoot
        {
            get
            {
                if (String.IsNullOrEmpty(rp.TrustRoot)) { rp.TrustRoot = Utility.WebRoot + "/"; }
                return rp.TrustRoot;
            }
            set { rp.TrustRoot = value; }
        }
        /// <summary>
        /// Gets or sets a URL to transfer user upon approval - defaults to current page
        /// </summary>
        public string ReturnURL
        {
            get { return rp.ReturnToURL; }
            set { rp.ReturnToURL = value; }
        }
        /// <summary>
        /// Gets or sets the OpenID idenitifer and normalizes the value
        /// </summary>
        public string Identity
        {
            get { return rp.Identity; }
            set
            {
                Utility.UpdateIdentity(value, rp);
            }
        }
        /// <summary>
        /// Array containing IDiscovery objects to use when performing name discovery.
        /// </summary>
        public IDiscovery[] PluginsDiscovery
        {
            get { return rp.DiscoveryPlugins; }
        }
        /// <summary>
        /// Array containing IExtension objects to use when performing authentication requests.
        /// </summary>
        public IExtension[] PluginsExtension
        {
            get { return rp.ExtensionPlugins; }
        }
        /// <summary>
        /// Gets or sets the URL of Identity Provider
        /// </summary>
        /// <remarks>
        /// If UseDirectedIdentity is set to true, OpenIDServer should be set manually.  Otherwise, this will be
        /// automatically set automatically while discovering the claimed identifier.
        /// </remarks>
        public string OpenIDServer
        {
            get { return rp.OpenIDServer; }
            set { rp.OpenIDServer = value; }
        }
        /// <summary>
        /// Authentication mode - either Stateful or Stateless
        /// </summary>
        public AuthenticationMode AuthMode
        {
            get
            {
                return rp.AuthMode;
            }
            set { rp.AuthMode = value; }
        }
        /// <summary>
        /// For Stateful mode, choose whether or not the shared secret will be encrypted
        /// </summary>
        public KeyEncryption Encryption
        {
            get
            {
                return rp.Encryption;
            }
            set { rp.Encryption = value; }
        }

        /// <summary>
        /// Checks the current page request and returns the requested
        /// mode.
        /// </summary>
        /// <returns>RequestedMode representing the current mode.</returns>
        public RequestedMode RequestedMode
        {
            get
            {
                return Utility.GetRequestedMode(rp.RequestArguments);
            }
        }
        /// <summary>
        /// Version of protocol currently being used.
        /// </summary>
        public ProtocolVersion AuthVersion
        {
            get
            {
                return rp.AuthVersion;
            }
            set
            {
                rp.AuthVersion = value;
            }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Provides a new OpenIDConsumer object with default settings.
        /// </summary>
        public OpenIDConsumer()
        {
            EnsureASPNetOnConstructor();
            rp = new StateContainer();
            rp.RequestArguments = HttpContext.Current.Request.Params;
            Init();
        }

        /// <summary>
        /// Provides a new OpenIDConsumer object using a custom set of request arguments.
        /// </summary>
        /// <param name="requestArguments">NameValueCollection containing arguments for this request.</param>
        public OpenIDConsumer(NameValueCollection requestArguments)
        {
            EnsureASPNetOnConstructor();
            rp = new StateContainer();
            rp.RequestArguments = requestArguments;
            Init();
        }
        /// <summary>
        /// Creates a new OpenIDConsumer using a custom set of request arguments.  Use this one for non-ASP.NET environments.
        /// </summary>
        /// <param name="assocmgr">Association persistence manager.</param>
        /// <param name="sessionmgr">Session persistence manager.</param>
        /// <param name="requestArguments">Arguments for the current request.</param>
        public OpenIDConsumer(NameValueCollection requestArguments, IAssociationPersistence assocmgr, ISessionPersistence sessionmgr)
        {
            rp = new StateContainer(assocmgr, sessionmgr);

            rp.RequestArguments = requestArguments;
            Init();
        }

        /// <summary>
        /// Creates a new OpenIDConsumer.
        /// </summary>
        /// <param name="assocmgr">Association persistence manager.</param>
        /// <param name="sessionmgr">Session persistence manager.</param>
        public OpenIDConsumer(IAssociationPersistence assocmgr, ISessionPersistence sessionmgr)
        {
            EnsureASPNetOnConstructor();
            rp = new StateContainer(assocmgr, sessionmgr);
            rp.RequestArguments = HttpContext.Current.Request.Params;
            Init();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Check for ASP.NET Context. If none is present, throw exception.
        /// </summary>
        /// <exception cref="InvalidOperationException">Thrown if ASP.NET Context is not present.</exception>
        private void EnsureASPNetOnConstructor()
        {
            if (HttpContext.Current == null)
            {
                throw new InvalidOperationException("Cannot initialize with this constructor if not in an ASP.NET application.  Use 'OpenIDConsumer(NameValueCollection, IAssociationPersistence, ISessionPersistence)' constructor instead.");                
            }
        }

        /// <summary>
        /// Redirects the user to a URL.
        /// </summary>
        /// <param name="url">URL to redirect</param>
        private void Redirect(string url)
        {
            if (HttpContext.Current != null)
            {
                HttpContext.Current.Response.Redirect(url, false);
            }
        }

        /// <summary>
        /// Shared initialization method - should be used by constructor.
        /// </summary>
        private void Init()
        {
            Tracer.Write("Initializing OpenID Consumer");
            this.TrustRoot = Utility.WebRoot + "/";

            if (HttpContext.Current != null)
            {
                NameValueCollection arguments = HttpContext.Current.Request.QueryString;

                StringBuilder qbuilder = new StringBuilder();

                for (int i = 0; i < arguments.Count; i++)
                {
                    string key = arguments.Keys[i];
                    bool shouldAdd = true;

                    if (String.IsNullOrEmpty(key)) { }
                    else if (key.StartsWith("openid.") || key == "cnonce") { shouldAdd = false; }

                    if (shouldAdd)
                    {
                        if (qbuilder.Length == 0)
                        {
                            qbuilder.Append("?");
                        }
                        else
                        {
                            qbuilder.Append("&");
                        }
                        qbuilder.Append(key + "=" + HttpUtility.UrlEncode(arguments[i]));
                    }
                }

                string queryString = qbuilder.ToString();
                this.ReturnURL = Utility.WebRoot
                    + HttpContext.Current.Request.ServerVariables["SCRIPT_NAME"]
                    + queryString;
            }

            new Plugins.Discovery.XRDS(rp);
            new Plugins.Discovery.Yadis(rp);
            new Plugins.Discovery.HTML(rp);

            rp.AuthMode = AuthenticationMode.Stateful;
            rp.Encryption = KeyEncryption.DHSHA256;

            if (rp.AssociationManager != null)
            {
                rp.AssociationManager.Cleanup();
            }
            Utility.AutoLoadExtensionPlugins(rp);

            Tracer.Write("Finished initialization");
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Performs a standard OpenID authentication request, and automatically redirects
        /// the current User Agent to the OpenID Provider.
        /// </summary>
        /// <returns>The URL to which the User Agent has been redirected.</returns>
        public string BeginAuth()
        {
            return BeginAuth(false, true);
        }

        /// <summary>
        /// Performs an OpenID authentiction request.
        /// </summary>
        /// <param name="immediate">Create an immediate-mode request URL.</param>
        /// <param name="autoRedirect">Automatically redirect the current HTTP session to the OpenID Provider.</param>
        public string BeginAuth(bool immediate, bool autoRedirect)
        {
            Tracer.Write("Beginning standard authentication check");
            // Check to ensure Identity is set, and the server name
            // can be retrieved.
            if (String.IsNullOrEmpty(Identity))
            {
                rp.ErrorState = Errors.NoIdSpecified;
                Tracer.Write("No OpenID specified, ending check");
                return null;
            }

            if (UseDirectedIdentity == false)
            {
                OpenIDServer = Utility.GetOpenIDServer(rp);
            }

            if (String.IsNullOrEmpty(OpenIDServer))
            {
                if (!IsError())
                {
                    rp.ErrorState = Errors.NoServersFound;
                    Tracer.Write("No OpenID Server found.");
                }
                return null;
            }

            Tracer.Write("OpenID Version Discovered: " + AuthVersion.ToString());

            AuthMode = Quirks.CheckOpenIDMode(rp.IdentityURL, AuthMode);

            // Perform stateful (smart) authentication
            if (rp.AuthMode == AuthenticationMode.Stateful)
            {
                if (OpenIDServer != "")
                {
                    if (Utility.BuildAssociation(OpenIDServer, AuthVersion, Encryption, rp.AssociationManager) == true)
                    {
                        string redirectUrl = Utility.GetRedirectURL(rp, immediate);
                        Tracer.Write("Returning Stateful URL - " + redirectUrl);
                        if (autoRedirect) { Redirect(redirectUrl); }
                        return redirectUrl;
                    }
                    else
                    {
                        rp.AuthMode = AuthenticationMode.Stateless;
                        Tracer.Write("Stateful key exchange failed, forcing Dumb mode and re-running authentication");
                    }
                }
            }

            // Perform stateless (dumb) authentication
            if (rp.AuthMode == AuthenticationMode.Stateless)
            {
                if (OpenIDServer != "")
                {
                    string redirectUrl = Utility.GetRedirectURL(rp, immediate);
                    Tracer.Write("Returning Immediate Stateless URL - " + redirectUrl);
                    if (autoRedirect) { Redirect(redirectUrl); }
                    return redirectUrl;
                }
            }
            return null;
        }
        /// <summary>
        /// Validates an OpenID authentication response.
        /// </summary>
        /// <remarks>
        /// <para>
        /// To determine if this method should be used, look at the value
        /// of the RequestedMode property, which detects the operational mode
        /// requested by the current HTTP request.  
        /// </para>
        /// <para>
        /// If RequestedMode is set to RequestedMode.IdResolution, the request
        /// is an authentication response from an OpenID Provider.
        /// </para>
        /// <para>
        /// Therefore, either Validate() should be used to verify
        /// the validity of the response.
        /// </para>
        /// </remarks>
        /// <returns>True if successfully authenticated, false if not.</returns>
        public bool Validate()
        {
            if (!Utility.VerifyReceivedIdentity(rp)) 
            {
                Tracer.Write("OpenID Provider sent a positive assertion for an OpenID for which it is not authoritative.");
                return false; 
            }

            string server = rp.OpenIDServer;
            if (String.IsNullOrEmpty(server))
            {
                server = Utility.GetOpenIDServer(rp);
            }

            if (rp.AuthMode == AuthenticationMode.Stateful)
            {
                Tracer.Write("Stateful mode enabled, beginning validation check using shared key");
                bool success = Utility.ValidateStatefulRequest(server, rp);
                rp.Cnonce = -1;
                if (success)
                {
                    return true;
                }
                else
                {
                    Tracer.Write("Validation failed, performing stateless validation check");
                    rp.AuthMode = AuthenticationMode.Stateless;
                }
            }

            if (rp.AuthMode == AuthenticationMode.Stateless)
            {
                Tracer.Write("Stateless mode enabled, beginning validation request with server");
                if (String.IsNullOrEmpty(Identity)) { return false; };
                Tracer.Write("Beginning stateless validation");
                return Utility.ValidateStatelessRequest(server, false, rp);
            }
            Tracer.Write("Request refused, authentication failed");
            rp.ErrorState = Errors.RequestRefused;
            return false;
        }
        /// <summary>
        /// Converts a supplied OpenID into two distinct entities - a normalized name and a URI
        /// </summary>
        /// <param name="openid">OpenID to normalize</param>
        /// <returns>A string array with two fields - the normalized name (extremeswank.com), 
        /// and the URI (http://extremeswank.com).</returns>
        public string[] Normalize(string openid)
        {
            return Utility.Normalize(openid, PluginsDiscovery);
        }
        /// <summary>
        /// Returns a string describing the current error state
        /// </summary>
        /// <returns></returns>
        public Errors GetError()
        {
            return rp.ErrorState;
        }
        /// <summary>
        /// Gets a boolean value stating whether or not an error condition exists
        /// </summary>
        /// <returns></returns>
        public bool IsError()
        {
            if (rp.ErrorState != Errors.NoErrors)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// After successful validation, provides an object to hold the user information
        /// </summary>
        /// <returns>OpenIDUser object containing identifier and Extension data</returns>
        public OpenIDUser RetrieveUser()
        {
            OpenIDUser ret = new OpenIDUser(Identity);
            ret.Retrieve(this);
            return ret;
        }
        /// <summary>
        /// Independently performs discovery on the supplied OpenID and determines whether
        /// or not it is valid.
        /// </summary>
        /// <returns>True if discovery was successful, false if not.</returns>
        public bool IsValidID()
        {
            string server = Utility.GetOpenIDServer(rp);

            if (String.IsNullOrEmpty(server)) { return false; }
            return true;
        }

        /// <summary>
        /// Gets the current StateContainer object in use by this OpenIDConsumer.
        /// </summary>
        /// <remarks>
        /// This should only need to be used by plugins so they can self-register
        /// upon intialization.
        /// </remarks>
        public StateContainer StateContainer
        {
            get { return rp; }
        }

        /// <summary>
        /// Look at the current request arguments and perform work appropriately,
        /// invoking events as conditions occur.
        /// </summary>
        public void HandleResponses()
        {
            RequestedMode rm = Utility.GetRequestedMode(rp.RequestArguments);

            EventArgs e = new EventArgs();
            switch (rm)
            {
                case RequestedMode.IdResolution:
                    if (ResponseReceived != null)
                    {
                        ResponseReceived.Invoke(this, e);
                    }
                    if (ProcessedIdResolutionResponse != null)
                    {
                        ProcessingIdResolutionResponse(this, e);
                    }
                    bool validated = Validate();
                    if (validated)
                    {
                        if (ValidationSucceeded != null)
                        {
                            ValidationSucceeded(this, e);
                        }
                    }
                    else 
                    {
                        if (ValidationFailed != null)
                        {
                            ValidationFailed(this, e);
                        }
                    }
                    if (ProcessedIdResolutionResponse != null)
                    {
                        ProcessedIdResolutionResponse(this, e);
                    }
                    break;
                case RequestedMode.CancelledByUser:
                    if (ResponseReceived != null)
                    {
                        ResponseReceived.Invoke(this, e);
                    }
                    if (ReceivedCancelResponse != null)
                    {
                        ReceivedCancelResponse.Invoke(this, e);
                    }
                    break;
                case RequestedMode.SetupNeeded:
                    if (ResponseReceived != null)
                    {
                        ResponseReceived.Invoke(this, e);
                    }
                    if (SetupNeeded != null)
                    {
                        SetupNeeded.Invoke(this, e);
                    }
                    break;
            }
        }

        #endregion

        #region Public Events

        /// <summary>
        /// A response has been received from an OpenID Provider.
        /// </summary>
        public event EventHandler ResponseReceived;

        /// <summary>
        /// A user-initiated Cancel response has been received from an OpenID Provider.
        /// </summary>
        public event EventHandler ReceivedCancelResponse;

        /// <summary>
        /// An authentication response has been received from an OpenID Provider,
        /// but no work has been done yet.
        /// </summary>
        public event EventHandler ProcessingIdResolutionResponse;

        /// <summary>
        /// An authentication response has been received from an OpenID Provider,
        /// and all work has been completed.
        /// </summary>
        public event EventHandler ProcessedIdResolutionResponse;

        /// <summary>
        /// The authentication response has been validated successfully.
        /// </summary>
        public event EventHandler ValidationSucceeded;

        /// <summary>
        /// The authentication response has failed validation.
        /// </summary>
        public event EventHandler ValidationFailed;

        /// <summary>
        /// Immediate mode request has failed, should issue a standard
        /// authentication request.
        /// </summary>
        public event EventHandler SetupNeeded;

        #endregion
    }
}
